/*!
* jProfile - JavaScript Performance Profile Tool for jQuery-enabled Site.
* http://code.google.com/p/jprofile/
* Inspired by John Resig article: http://ejohn.org/blog/deep-profiling-jquery-apps/
*
* Copyright 2011, sanshi.me
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jmarker.googlecode.com/svn/trunk/license/license.htm
* 
* Version: 1.2
* Date: Aug 2, 2011
*/
(function($){
  
	// Get or set cookie.
	function cookie(name, value) {
		if(value === undefined) {
			var returned;
			$.each(document.cookie.split(';'), function(index, item) {
				var tmp = $.trim(item).split('=');
				if(name === tmp[0]) {
					returned = decodeURIComponent(tmp[1]);
					return false;
				}
			});
			return returned;
		} else {
			var expires = '', secure = '';
			if(value === null) {
				expires = ';expires=' + new Date(2000, 1, 1).toUTCString();
			}
			if(window.location.protocol === 'https:') {
				secure = ';secure=1';
			}
			document.cookie = [name, '=', encodeURIComponent(value), ';path=/', expires, secure].join('');
		}
	}
	
	// Get function name.
	function getFunctionName(fn)
	{
		var name = /\W*function\s+([\w\$]+)\(/.exec(fn);
		if(name) {
			return name[1];
		}
		return '';
	}
	
	// Inspired by http://ejohn.org/blog/deep-profiling-jquery-apps/.
	// Form elem to string.
	function formatElem(elem) {
		var str = "";
		if (elem.tagName) {
			str = "&lt;" + elem.tagName.toLowerCase();
			if (elem.id) str += "#" + elem.id;
			if (elem.className) str += "." + elem.className.replace(/ /g, ".");
			str += "&gt;";
		} else {
			str = elem.nodeName;
		}
		return str;
	}
	
	// Inspired by http://ejohn.org/blog/deep-profiling-jquery-apps/.
	// Format function arguments.
	function formatArgs(args) {
		var str = [];
		for (var i = 0; i < args.length; i++) {
			var item = args[i];

			if (item && item.constructor == Array) {
				str.push("Array(" + item.length + ")");
			} else if (item && item.jquery) {
				str.push("jQuery(" + item.length + ")");
			} else if (item && item.nodeName) {
				str.push(formatElem(item));
			} else if (item && typeof item == "function") {
				str.push("function()");
			} else if (item && typeof item == "object") {
				str.push("{...}");
			} else if (typeof item == "string") {
				str.push('"' + item.replace(/&/g, "&amp;").replace(/</g, "&lt;") + '"');
			} else {
				str.push(item + "");
			}
		}
		return str.join(", ");
	}
	
	// Borrow from jQuery source code.
	var class2type = [], types = "Boolean Number String Function Array Date RegExp Object".split(" "),
		i, count = types.length, name;
	
	// Populate the class2type map
	for(i=0; i<count; i++) {
		name = types[i];
		class2type[ "[object " + name + "]" ] = name.toLowerCase();
	}
	
	function getType(obj) {
		return obj == null ?
			String(obj) :
			class2type[ Object.prototype.toString.call(obj) ] || "object";
	}
	
	// Emit messages to console.
	function log() {
		/*
        if (typeof(console) !== "undefined") {
            for(var i = 0; i < arguments.length; i++) {
                console.log('[jProfile] ' + arguments[i]);
            }
        }
		*/
    }
	
	function initProfile() {
		var cookiejprofile = cookie('jprofile'), namespaces, fullnames = [], i, count, namespace;
		if(initialized || !cookiejprofile) {
			return;
		}
		
		function backupFN(fn, fullname, name, parent) {
			var old = fn;
			
			fullnames.push(fullname);
			parent[name] = function(){
				if(!stopped) {
					var args = formatArgs(arguments);
					var start = (new Date).getTime();
					var ret;
					// If this function call is through "new" operator.
					// Sometimes arguments.callee is undefined, for example the jQuery.isArray
					if(arguments.callee.prototype && this instanceof arguments.callee) {
						// http://stackoverflow.com/questions/1606797/use-of-apply-with-new-operator-is-this-possible
						function F(args2) {
							return old.apply(this, args2);
						}
						F.prototype = old.prototype;
						ret = new F(arguments);
					} else {
						ret = old.apply(this, arguments);
					}
					var	time = (new Date).getTime() - start;
					var detail = {
						time: time,
						args: args
					};
					if(logs[fullname]) {
						logs[fullname].details.push(detail);
					} else {
						logs[fullname] = {
							calls: 0,
							total: 0,
							avg: 0,
							min: 0,
							max: 0,
							details: [detail]
						};
					}
				} else {
					ret = old.apply(this, arguments);
				}
				return ret;
			}
			
			// Function can also has methods, don't forget these methods.
			for(var itemname in old) {
				parent[name][itemname] = old[itemname];
			}
			
			// And prototype property.
			parent[name].prototype = old.prototype;
		}
		
		function checkObj(obj, fullname, name, parent) {
			var type = getType(obj), itemname, item;
			if(type === 'function') {
				backupFN(obj, fullname, name, parent);
			}
			if(obj && type !== 'array' && type !== 'string' && type !== 'number' && getType(obj.hasOwnProperty) === 'function') {
				for (itemname in obj) {
					if(obj.hasOwnProperty(itemname)) {
						item = obj[itemname];
						// window.window === window
						// jQuery.fn.constructor === jQuery will introduce endless loop
						if(item !== obj && itemname !== 'constructor') {
							checkObj(item, fullname + '.' + itemname, itemname, obj);
						}
					}
				}					
			}
		}
		
		initialized = true;
		namespaces = cookiejprofile.replace(/,/ig, ';').split(';');
		count = namespaces.length;
		for(i=0; i<count; i++) {
			namespace = $.trim(namespaces[i]);
			var obj = eval(namespace), 
				fullname = namespace, 
				name = namespace, 
				parent = window, 
				dotIndex = fullname.indexOf('.');
			if(dotIndex >= 0) {
				name = fullname.substr(dotIndex + 1);
				parent = eval(fullname.substr(0, dotIndex));
			}
			checkObj(obj, fullname, name, parent);
		}
		log('Backup Functions: ' + fullnames.join('; '));
	}
	
	// Init profile automatically.
	var initialized = false; logs = {}, stopped = false;
	initProfile();
	
	// Construct UI display.
	$(function() {
		$('head').append('<style type="text/css">' +
			'#jprofile{ position: absolute; top: 5px; right: 5px; z-index: 10000; font-size: 13px; line-height: 1.3em; }' +
			'#jprofile a{ color: #52727F; text-decoration: none; }' +
			'#jprofile table td{ text-align: left; border-bottom: solid 1px #eee; padding-top: 5px; }' +
			'#jprofile table th{ text-align: left; border-bottom: solid 2px #ddd; }' +
			'#jprofile_disabled{ background-color: #fff; padding: 10px; border-radius: 5px; box-shadow: 0 0 5px red; border: 2px solid red; display: block; }' +
			'#jprofile_enabled{ background-color: #fff; padding: 10px; border-radius: 5px; box-shadow: 0 0 5px green; border: 2px solid green; display: none; text-align: right; }' +
			'#jprofile_display{ width: 550px; display: none; min-height: 300px; margin-top: 5px; padding-top: 5px; }' +
			'#jprofile_display table{ width: 100%; text-align: left; }' +
			'#jprofile_display table td.name a{ }' +
			'#jprofile_display .nologs{ color: #333333; font-size: 1.2em; margin-top: 100px; text-align: center; }' +
			'#jprofile_detail { background-color: #fff; border: 2px solid #999; border-radius: 5px; box-shadow: 0 0 10px #ccc; padding: 10px; position: absolute; min-width: 500px; display: none; max-width: 650px; overflow: auto; }' +
			'#jprofile_detail table{ width: 100%; text-align: left; }' +
			'</style>');
		
		var html = [];
		html.push('<div id="jprofile">');
		html.push('<div id="jprofile_disabled">');
		html.push('<input class="enable" type="button" value="Enable jProfile" title="Enable jProfile"/>');
		html.push('</div>');
		html.push('<div id="jprofile_enabled">');
		html.push('<input class="start" type="button" disabled="disabled" value="Start" title="Start jProfile"/>');
		html.push('<input class="stop" type="button" value="Stop" title="Stop profile"/>');
		html.push('<input class="clearlogs" type="button" value="Clear" title="Clear current logs"/>');
		html.push('<input class="display" type="button" value="Display" title="Display the logs"/>');
		html.push('<input class="disable" type="button" value="Disable jProfile" title="Disable jProfile"/>');
		html.push('<div id="jprofile_display">');
		html.push('</div>');
		html.push('</div>');
		html.push('<div id="jprofile_detail">');
		html.push('</div>');
		html.push('</div>');
		$(document.body).append(html.join(''));
		
		var disabledContainer = $('#jprofile_disabled'),
			enabledContainer = $('#jprofile_enabled'),
			displayContainer = $('#jprofile_display'),
			detailContainer = $('#jprofile_detail'),
			enableNode = disabledContainer.find('.enable'),
			startNode = enabledContainer.find('.start'),
			stopNode = enabledContainer.find('.stop');
		
		if(cookie('jprofile')) {
			disabledContainer.hide();
			enabledContainer.show();
		}
		
		enableNode.click(function() {
			var profileNS = $.trim(window.prompt('Please input namespaces you want to profile, separate multi-namespaces by semicolon or comma:'));
			if(profileNS) {
				cookie('jprofile', profileNS);
				window.location.reload(true);
				/*
				disabledContainer.hide();
				enabledContainer.show();
				initProfile();
				*/
			}
		});
		
		enabledContainer.find('.disable').click(function() {
			cookie('jprofile', null);
			window.location.reload(true);
			/*
			stopped = true;
			logs = {};
			disabledContainer.show();
			enabledContainer.hide();
			*/
		});	

		startNode.click(function() {
			stopped = false;
			startNode.attr('disabled', true);
			stopNode.attr('disabled', false);
		});

		stopNode.click(function() {
			stopped = true;
			startNode.attr('disabled', false);
			stopNode.attr('disabled', true);
		});			
		
		enabledContainer.find('.clearlogs').click(function() {
			logs = {};
			displayContainer
				.data('sort-data', jProfile())
				.data('sort-by', 'total')
				.data('sort-order', 'desc');
			if(displayContainer.is(':visible')) {
				showDisplayTable();
			}
		});
		
		enabledContainer.find('.display').click(function() {
			if(displayContainer.is(':visible')) {
				detailContainer.hide();
				displayContainer.hide();
			} else {
				displayContainer
					.data('sort-data', jProfile())
					.data('sort-by', 'total')
					.data('sort-order', 'desc');
				showDisplayTable();
			}
		});
		
		displayContainer.delegate('th[data-sort-by] a', 'click', function() {
			var th = $(this).parent('th'),
				sortBy = th.attr('data-sort-by'),
				data = displayContainer.data('sort-data'),
				sortOrder = 'desc';
			if(!th.hasClass('desc') && !th.hasClass('asc')) {
				th.siblings().removeClass('desc').removeClass('asc').end().addClass('desc');
				sortOrder = 'desc';
			} else if(th.hasClass('desc')) {
				th.removeClass('desc').addClass('asc');
				sortOrder = 'asc';
			} else {
				th.removeClass('asc').addClass('desc');
				sortOrder = 'desc';
			}
			
			data.sort(function(a, b) {
				if(sortOrder === 'desc') {
					return b[sortBy] - a[sortBy];
				} else {
					return a[sortBy] - b[sortBy];
				}
			});
			displayContainer
				.data('sort-data', data)
				.data('sort-by', sortBy)
				.data('sort-order', sortOrder);
			showDisplayTable();
		});
		
		displayContainer.delegate('td.name a', 'click', function() {
			var html = [], tr = $(this).parents('tr'), trposition, 
				sortData = displayContainer.data('sort-data'),
				details = sortData[tr.index() - 1].details;
			
			details.sort(function(a, b) {
				return b.time - a.time;
			});
			html.push('<table class="details">');
			html.push('<tr>');
			html.push('<th style="width: 50px;">Time</th>');
			html.push('<th>Args</th>');
			html.push('</tr>');
			$.each(details, function(index, detail) {
				html.push('<tr>');
				html.push('<td>'+ detail.time +'</td>');
				html.push('<td>'+ detail.args +'</td>');
				html.push('</tr>');
			});
			html.push('</table>');
			detailContainer.html(html.join(''));
			
			trposition = tr.position();
			detailContainer.css({
				top: trposition.top,
				right: enabledContainer.outerWidth() + 5
			}).show();
		});
		
		function showDisplayTable() {
			var html = [], 
				sortData = displayContainer.data('sort-data'),
				sortBy = displayContainer.data('sort-by'),
				sortOrder = displayContainer.data('sort-order');
			if(sortData.length) {
				html.push('<table class="main">');
				html.push('<tr>');
				$.each([['name', 'Name'], ['calls', 'Calls'], ['total', 'Total'], ['avg', 'Avg'], ['min', 'Min'], ['max', 'Max']], function(index, item) {
					html.push('<th data-sort-by="');
					html.push(item[0]);
					html.push('"');
					if(item[0] === sortBy) {
						html.push(' class="'+ sortOrder +'"');
					}
					html.push('><a href="javascript:;">');
					html.push(item[1]);
					html.push('</a></th>');
				});
				html.push('</tr>');
				$.each(sortData, function(index, item) {
					html.push('<tr>');
					html.push('<td class="name"><a href="javascript:;">'+ item.name +'</a></td>');
					html.push('<td>'+ item.calls +'</td>');
					html.push('<td>'+ item.total +'</td>');
					html.push('<td>'+ item.avg +'</td>');
					html.push('<td>'+ item.min +'</td>');
					html.push('<td>'+ item.max +'</td>');
					html.push('</tr>');
				});
				html.push('</table>');
			} else {
				html.push('<div class="nologs">There is no logs.</div>');
			}
			displayContainer.html(html.join('')).show();
			detailContainer.hide();
		}
		
		// Enable jProfile use the Ctrl+0
		$(document).keydown(function(event) {
			if(event.ctrlKey && event.keyCode === 48) {
				enableNode.click();
			}
		});
		
	});
	
	// Public interfaces.
	window.jProfile = function(sortfield) {
		var logArray = [];
		$.each(logs, function(logname, log) {
			var calls = log.details.length, alltime = 0, avg = 0, min = 1000, max = 0;
			$.each(log.details, function(index, detail){
				var time = detail.time
				alltime += time;
				if(time <= min) {
					min = time;
				}
				if(time >= max) {
					max = time;
				}
			});
			
			logArray.push({
				name: logname,
				calls: calls,
				total: alltime,
				avg: parseInt(alltime / calls, 10),
				min: min,
				max: max,
				details: log.details
			});
		});
		logArray.sort(function(a, b) {
			return b.total - a.total;
		});
		return logArray;
	};
	
	
})(jQuery);
